// ============================================
// India20Sixty Dashboard v3.0
// Frontend for API Worker
// ============================================
// CONFIG - Update this to your API worker URL
const API_BASE = 'https://api.india20sixty.workers.dev';
// Constants
const CATS = {
AI: { label: 'AI & ML', color: '#00e5ff', emoji: '๐ค' },
Space: { label: 'Space & Defence', color: '#b388ff', emoji: '๐' },
Gadgets: { label: 'Gadgets & Tech', color: '#ffd740', emoji: '๐ฑ' },
DeepTech: { label: 'Deep Tech', color: '#ff6b35', emoji: '๐ฌ' },
GreenTech: { label: 'Green & Energy', color: '#00e676', emoji: 'โก' },
Startups: { label: 'Startups', color: '#ff6b9d', emoji: '๐ก' }
};
const VPD_SCHEDULES = {
1: ['12:00 PM IST'],
2: ['6:00 AM IST', '6:00 PM IST'],
3: ['6:00 AM IST', '12:00 PM IST', '6:00 PM IST']
};
const PROG = {
pending: 8, processing: 35, images: 55, voice: 68, render: 82,
upload: 93, staged: 20, mixing: 90, complete: 100, test_complete: 100, failed: 0
};
const PCOL = {
pending: '#ffd740', processing: '#00e5ff', images: '#b388ff', voice: '#b388ff',
render: '#ff6b35', upload: '#00e5ff', staged: '#ffd740', mixing: '#ff6b35',
complete: '#00e676', test_complete: '#00e676', failed: '#ff5252'
};
const BLBL = {
pending: 'Pending', processing: 'Processing', images: 'Images', voice: 'Voice',
render: 'Rendering', upload: 'Uploading', staged: 'Staged', mixing: 'Mixing',
complete: 'Complete', test_complete: 'Complete', failed: 'Failed'
};
const CHAR = {
natural: 'No pitch shift',
woman: '+4 st, formant +1.2',
man: '-2 st deeper',
elder: '-4 st, formant -0.8',
child: '+9 st, formant +2.0',
radio: 'Heavy compression + reverb'
};
// State
let activeTab = 'all';
let allJobs = [];
let allTopics = [];
let allAnalytics = [];
let analyticsJobs = [];
let topicFilter = 'ready';
let currentPage = 'home';
let activeCat = 'all';
let topicCat = 'all';
let currentVoiceMode = 'ai';
let calDate = new Date();
let calEvents = [];
let studioJob = null;
let mediaRecorder = null;
let audioChunks = [];
let recordedBlob = null;
let audioCtx = null;
let analyserNode = null;
let recTimer = null;
let recSecs = 0;
let selectedMusic = null;
let selectedPreset = 'natural';
let playbackAudio = null;
let isRecording = false;
let currentStagingTab = 'staged';
let allCBDP = [];
let allStaged = [];
let selectedImages = [];
let libTopicFilter = 'all';
let allImages = [];
let R2_BASE_URL = '';
// ============================================
// UTILS
// ============================================
function ago(iso) {
const s = Math.floor((Date.now() - new Date(iso)) / 1000);
if (s < 60) return s + 's';
if (s < 3600) return Math.floor(s / 60) + 'm';
if (s < 86400) return Math.floor(s / 3600) + 'h';
return Math.floor(s / 86400) + 'd';
}
function fmt(n) {
if (!n) return '0';
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return String(n);
}
function scClass(s) {
return s >= 80 ? 'sc-hi' : s >= 60 ? 'sc-med' : 'sc-lo';
}
function badge(st) {
const s = st || 'unknown';
const dot = ['pending', 'processing'].includes(s) ? ' ' : '';
return `${dot}${BLBL[s] || s} `;
}
function showDebug(id, html) {
const el = document.getElementById(id);
if (el) el.innerHTML = `
${html}
`;
}
// ============================================
// API CALLS
// ============================================
async function apiGet(endpoint) {
const r = await fetch(`${API_BASE}${endpoint}`);
if (!r.ok) throw new Error(`API ${r.status}: ${await r.text()}`);
return r.json();
}
async function apiPost(endpoint, body) {
const r = await fetch(`${API_BASE}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!r.ok) throw new Error(`API ${r.status}: ${await r.text()}`);
return r.json();
}
// ============================================
// PAGE NAVIGATION
// ============================================
function showPage(name, btn) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById('page-' + name).classList.add('active');
btn.classList.add('active');
currentPage = name;
if (name === 'staging') { loadStaging(); renderStagingGrid(); }
if (name === 'review') { loadCBDPReview(); }
if (name === 'library') { loadLibrary(); }
if (name === 'calendar') { loadCalendar(); renderCalendar(); }
if (name === 'analytics') { renderAnalytics(); }
if (name === 'topics') { renderTopicsPage(); }
}
// ============================================
// CATEGORY STRIPS
// ============================================
function buildCatStrips() {
const s = document.getElementById('cat-strip');
const ts = document.getElementById('topic-cat-strip');
const mc = document.getElementById('modal-cats');
Object.keys(CATS).forEach(k => {
const cat = CATS[k];
// Home filter
const p = document.createElement('div');
p.className = 'cat-pill';
p.dataset.cat = k;
p.innerHTML = `${cat.emoji} ${cat.label} 0 `;
p.onclick = () => filterByCat(k, p);
s.appendChild(p);
// Topics filter
const p2 = p.cloneNode(true);
p2.onclick = () => filterTopicsByCat(k, p2);
ts.appendChild(p2);
// Modal categories
const d = document.createElement('div');
d.className = 'cat-check selected';
d.dataset.cat = k;
d.innerHTML = `${cat.emoji} ${cat.label} `;
d.onclick = () => d.classList.toggle('selected');
mc.appendChild(d);
});
}
function filterByCat(cat, btn) {
activeCat = cat;
document.querySelectorAll('#cat-strip .cat-pill').forEach(p => {
const isA = p.dataset.cat === cat;
p.classList.toggle('active', isA);
p.style.borderColor = isA ? (cat !== 'all' ? CATS[cat].color : 'var(--accent)') : '';
p.style.color = isA ? (cat !== 'all' ? CATS[cat].color : 'var(--accent)') : '';
});
renderJobs();
}
function filterTopicsByCat(cat, btn) {
topicCat = cat;
document.querySelectorAll('#topic-cat-strip .cat-pill').forEach(p => {
const isA = p.dataset.cat === cat;
p.classList.toggle('active', isA);
p.style.borderColor = isA ? (cat !== 'all' ? CATS[cat].color : 'var(--accent)') : '';
p.style.color = isA ? (cat !== 'all' ? CATS[cat].color : 'var(--accent)') : '';
});
renderTopicsPage();
}
function switchTab(tab) {
activeTab = tab;
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
renderJobs();
}
function switchStagingTab(tab) {
currentStagingTab = tab;
document.querySelectorAll('#stab-staged, #stab-cbdp').forEach(t => {
t.classList.toggle('active', t.id === 'stab-' + tab);
});
document.getElementById('stab-panel-staged').style.display = tab === 'staged' ? 'block' : 'none';
document.getElementById('stab-panel-cbdp').style.display = tab === 'cbdp' ? 'block' : 'none';
if (tab === 'cbdp') loadCBDPGrid();
}
// ============================================
// DATA LOADING
// ============================================
async function loadConfig() {
try {
const cfg = await apiGet('/config');
R2_BASE_URL = cfg.r2_base_url || '';
} catch (e) {
console.error('Config load failed:', e);
}
}
async function loadJobs() {
try {
allJobs = await apiGet('/jobs');
const run = allJobs.filter(j => ['pending', 'processing', 'images', 'voice', 'render', 'upload', 'staged', 'mixing'].includes(j.status));
const ok = allJobs.filter(j => j.status === 'complete' || j.status === 'test_complete');
const fail = allJobs.filter(j => j.status === 'failed');
document.getElementById('s-total').textContent = allJobs.length;
document.getElementById('s-running').textContent = run.length;
document.getElementById('s-complete').textContent = ok.length;
document.getElementById('s-failed').textContent = fail.length;
document.getElementById('tc-all').textContent = allJobs.length;
document.getElementById('tc-run').textContent = run.length;
document.getElementById('tc-ok').textContent = ok.length;
document.getElementById('tc-fail').textContent = fail.length;
document.getElementById('last-ref').textContent = 'Updated ' + new Date().toLocaleTimeString();
renderJobs();
} catch (e) {
console.error('loadJobs:', e);
showDebug('debug-home', 'Failed to load jobs: ' + e.message + ' ');
}
}
async function loadQueue() {
try {
allTopics = await apiGet('/topics?used=false&min_score=70');
const ready = allTopics.filter(t => !t.used && t.council_score >= 70);
document.getElementById('s-topics').textContent = ready.length;
Object.keys(CATS).forEach(k => {
const el = document.getElementById('cc-' + k);
if (el) el.textContent = ready.filter(t => t.cluster === k).length;
});
const el = document.getElementById('queue-list');
if (!ready.length) {
el.innerHTML = '๐ซ No topics. Click Replenish.
';
return;
}
el.innerHTML = ready.slice(0, 8).map(t => {
const cat = CATS[t.cluster];
return ``;
}).join('');
} catch (e) {
console.error('loadQueue:', e);
}
}
async function loadStaging() {
try {
allStaged = await apiGet('/staging');
const cnt = document.getElementById('stg-cnt');
const stc = document.getElementById('stc-staged');
if (cnt) {
cnt.textContent = allStaged.length;
cnt.style.display = allStaged.length ? 'inline-block' : 'none';
}
if (stc) stc.textContent = allStaged.length;
if (currentPage === 'staging') renderStagingGrid();
} catch (e) {
console.error('loadStaging:', e);
}
}
async function loadCBDP() {
try {
allCBDP = await apiGet('/cbdp');
const cnt = document.getElementById('stc-cbdp');
if (cnt) cnt.textContent = allCBDP.length;
if (currentPage === 'staging' && currentStagingTab === 'cbdp') loadCBDPGrid();
} catch (e) {
console.error('loadCBDP:', e);
}
}
async function loadCBDPReview() {
try {
const data = await apiGet('/review');
const allReview = Array.isArray(data) ? data : [];
const rc = document.getElementById('rev-cnt');
const rc2 = document.getElementById('cbdp-count');
if (rc) {
rc.textContent = allReview.length;
rc.style.display = allReview.length ? 'inline-block' : 'none';
}
if (rc2) rc2.textContent = allReview.length;
renderReviewGrid(allReview);
} catch (e) {
console.error('loadCBDPReview:', e);
}
}
async function loadLibrary() {
const el = document.getElementById('lib-grid');
if (el) el.innerHTML = 'โณ Loading images...
';
try {
allImages = await apiGet('/image-library');
const lc = document.getElementById('lib-count');
if (lc) lc.textContent = allImages.length;
buildLibFilter();
renderLibrary();
} catch (e) {
console.error('loadLibrary:', e);
if (el) el.innerHTML = `โ Failed to load: ${e.message}
`;
}
}
async function loadCalendar() {
try {
calEvents = await apiGet('/calendar');
if (currentPage === 'calendar') renderCalendar();
} catch (e) {
console.error('loadCalendar:', e);
}
}
async function loadAnalytics() {
try {
const data = await apiGet('/analytics');
allAnalytics = data.analytics || [];
analyticsJobs = data.jobs || [];
if (currentPage === 'analytics') renderAnalytics();
} catch (e) {
console.error('loadAnalytics:', e);
}
}
async function loadSystemState() {
try {
const state = await apiGet('/system-state');
setPublishUI(state.publish === true);
currentVoiceMode = state.voice_mode || 'ai';
setVoiceModeUI(currentVoiceMode);
const vpd = state.videos_per_day || 1;
document.querySelectorAll('.vpd-btn').forEach(b => {
const isA = b.id === 'vpd-' + vpd;
b.className = 'btn ' + (isA ? 'btn-primary' : 'btn-ghost') + ' vpd-btn';
});
document.getElementById('sched-times').textContent = (VPD_SCHEDULES[vpd] || []).join(' โข ');
document.getElementById('sched-desc').textContent = vpd + ' video' + (vpd > 1 ? 's' : '') + '/day';
} catch (e) {
console.error('loadSystemState:', e);
}
}
// ============================================
// RENDERING
// ============================================
function filterJobs(jobs, tab) {
let j = jobs;
if (activeCat !== 'all') j = j.filter(x => x.cluster === activeCat);
if (tab === 'running') return j.filter(x => ['pending', 'processing', 'images', 'voice', 'render', 'upload', 'staged', 'mixing'].includes(x.status));
if (tab === 'complete') return j.filter(x => x.status === 'complete' || x.status === 'test_complete');
if (tab === 'failed') return j.filter(x => x.status === 'failed');
return j;
}
function renderJobs() {
const el = document.getElementById('job-list');
const jobs = filterJobs(allJobs, activeTab);
if (!jobs.length) {
el.innerHTML = '๐ซ No jobs here.
';
return;
}
el.innerHTML = jobs.map(j => {
const prog = PROG[j.status] || 0;
const col = PCOL[j.status] || '#5a6278';
const cat = CATS[j.cluster];
const catBadge = cat ? `${cat.emoji} ${j.cluster} ` : '';
const yt = j.youtube_id && j.youtube_id !== 'TEST_MODE'
? `โถ Watch `
: (j.youtube_id === 'TEST_MODE' ? 'test ' : '');
const err = j.error ? `${j.error.slice(0, 35)} ` : '';
return `
${j.topic || 'Untitled'}
${catBadge}${j.council_score ? `${j.council_score} ` : ''}${err}${yt ? `${yt} ` : ''}
${badge(j.status)}
${j.updated_at ? ago(j.updated_at) + ' ago' : '-'}
`;
}).join('');
}
function renderStagingGrid() {
const el = document.getElementById('staged-grid');
if (!el) return;
if (!allStaged.length) {
el.innerHTML = `๐ฌ No staged videos. ${currentVoiceMode === 'human' ? 'Create a video and it will appear here.' : 'Switch to Human Voice mode first.'}
`;
return;
}
el.innerHTML = allStaged.map(j => {
const cat = CATS[j.cluster] || { color: 'var(--muted)', emoji: '๐ฌ', label: j.cluster || '?' };
const scr = (j.script_package && j.script_package.text) || '';
const ageStr = j.created_at ? ago(j.created_at) + ' ago' : '';
return `
${j.topic || 'Untitled'}
${cat.emoji} ${cat.label}
${j.council_score || 0}
${ageStr}
${scr.slice(0, 110)}${scr.length > 110 ? 'โฆ' : ''}
`;
}).join('');
}
function loadCBDPGrid() {
const el = document.getElementById('staging-cbdp-grid');
if (!el) return;
if (!allCBDP.length) {
el.innerHTML = '๐ No CBDP jobs.
';
return;
}
el.innerHTML = allCBDP.map(j => {
const cat = CATS[j.cluster] || { color: 'var(--muted)', emoji: '๐ฌ', label: j.cluster || '?' };
const age = j.created_at ? ago(j.created_at) + ' ago' : '';
return `
${j.topic || 'Untitled'}
${cat.emoji} ${cat.label}
${age}
${j.error || 'Upload failed'}
`;
}).join('');
}
function renderReviewGrid(allReview) {
const el = document.getElementById('cbdp-grid');
if (!el) return;
if (!allReview || !allReview.length) {
el.innerHTML = `
๐ฌ No videos in review queue.
Videos land here when PUBLISH is OFF, or when YouTube upload fails after a successful render.
`;
return;
}
try {
el.innerHTML = allReview.map(j => {
const cat = CATS[j.cluster] || { color: 'var(--muted)', emoji: '๐ฌ', label: j.cluster || '?' };
const scr = (j.script_package && j.script_package.text) || '';
const title = (j.script_package && j.script_package.title) || j.topic || 'Untitled';
const age = j.updated_at ? ago(j.updated_at) + ' ago' : '';
const reason = j.review_reason || 'Ready for review';
const hasVideo = !!(j.video_r2_url && R2_BASE_URL);
const videoUrl = hasVideo ? `${R2_BASE_URL}/${j.video_r2_url}` : '';
const statusColor = j.status === 'review' ? 'var(--accent)' : 'var(--yellow)';
const statusLabel = j.status === 'review' ? 'REVIEW' : 'CBDP';
return `
${title}
${cat.emoji} ${cat.label}
${j.council_score || 0}
${statusLabel}
${age}
${reason}
${hasVideo
? `
`
: `
๐ฌ No video file saved
This job failed before R2 save โ reject and re-run
`
}
${scr ? scr.slice(0, 140) + (scr.length > 140 ? 'โฆ' : '') : 'No script saved '}
`;
}).join('');
} catch (err) {
console.error('renderReviewGrid error:', err);
el.innerHTML = `โ Error: ${err.message}
`;
}
}
function buildLibFilter() {
const topics = [...new Set(allImages.map(i => i.topic || 'unknown'))].filter(Boolean);
const el = document.getElementById('lib-filter');
if (!el) return;
el.innerHTML = `All (${allImages.length})
`
+ topics.slice(0, 12).map(t => {
const count = allImages.filter(i => i.topic === t).length;
return `${t.slice(0, 25)} (${count})
`;
}).join('');
}
function filterLib(topic, el) {
libTopicFilter = topic;
document.querySelectorAll('#lib-filter .cat-pill').forEach(p => {
p.style.borderColor = '';
p.style.color = '';
});
if (el) {
el.style.borderColor = 'var(--accent)';
el.style.color = 'var(--accent)';
}
renderLibrary();
}
function renderLibrary() {
const el = document.getElementById('lib-grid');
if (!el) return;
const imgs = libTopicFilter === 'all' ? allImages : allImages.filter(i => i.topic === libTopicFilter);
if (!imgs.length) {
el.innerHTML = `
๐ผ No images yet.
Images are saved automatically when you create a video.
`;
return;
}
el.innerHTML = imgs.map((img, idx) => {
const sel = selectedImages.indexOf(img.url) > -1;
const selIdx = selectedImages.indexOf(img.url);
return `
${sel ? `
${selIdx + 1}
` : ''}
${img.topic.slice(0, 30)}
`;
}).join('');
}
function renderCalendar() {
const el = document.getElementById('cal-grid');
if (!el) return;
const y = calDate.getFullYear(), m = calDate.getMonth();
document.getElementById('cal-lbl').textContent = calDate.toLocaleDateString('en-IN', { month: 'long', year: 'numeric' });
const first = new Date(y, m, 1).getDay();
const days = new Date(y, m + 1, 0).getDate();
const today = new Date();
let html = '';
for (let i = 0; i < first; i++) html += '
';
for (let d = 1; d <= days; d++) {
const isToday = today.getDate() === d && today.getMonth() === m && today.getFullYear() === y;
const evts = calEvents.filter(e => {
const ed = new Date(e.scheduled_at || e.created_at);
return ed.getFullYear() === y && ed.getMonth() === m && ed.getDate() === d;
});
const evHtml = evts.map(e => {
const cat = CATS[e.cluster] || { color: 'var(--accent)' };
const t = e.scheduled_at ? new Date(e.scheduled_at).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' }) : '';
return `${t ? t + ' ' : ''}${(e.topic || '').slice(0, 14)}
`;
}).join('');
html += ``;
}
el.innerHTML = html;
}
function renderAnalytics() {
const rows = allAnalytics;
if (!rows.length) {
['a-views', 'a-likes', 'a-comments', 'a-avg'].forEach(id => {
const e = document.getElementById(id);
if (e) e.textContent = '-';
});
document.getElementById('video-grid').innerHTML = '๐ No analytics yet.
';
document.getElementById('perf-list').innerHTML = '';
return;
}
document.getElementById('a-views').textContent = fmt(rows.reduce((s, r) => s + (r.youtube_views || 0), 0));
document.getElementById('a-likes').textContent = fmt(rows.reduce((s, r) => s + (r.youtube_likes || 0), 0));
document.getElementById('a-comments').textContent = fmt(rows.reduce((s, r) => s + (r.comment_count || 0), 0));
document.getElementById('a-avg').textContent = fmt(rows.length ? Math.round(rows.reduce((s, r) => s + (r.score || 0), 0) / rows.length) : 0);
document.getElementById('a-count').textContent = rows.length + ' videos';
const sorted = [...rows].sort((a, b) => b.score - a.score);
document.getElementById('video-grid').innerHTML = sorted.map(r => {
const job = analyticsJobs.find(j => j.id === r.video_id) || {};
const hasYt = job.youtube_id && job.youtube_id !== 'TEST_MODE';
return `
๐ฌ
${job.topic || 'Unknown'}
๐ ${fmt(r.youtube_views || 0)}
โค ${fmt(r.youtube_likes || 0)}
${fmt(r.score || 0)}
${hasYt ? `
โถ Watch ` : ''}
`;
}).join('');
const perfRow = r => {
const j = analyticsJobs.find(x => x.id === r.video_id) || {};
return `
${j.topic || '-'}
${fmt(r.youtube_views || 0)}
${fmt(r.youtube_likes || 0)}
${fmt(r.score || 0)}
`;
};
document.getElementById('perf-list').innerHTML = sorted.slice(0, 5).map(perfRow).join('') || 'No data
';
}
function renderTopicsPage() {
let topics = allTopics;
if (topicFilter === 'ready') topics = topics.filter(t => !t.used && t.council_score >= 70);
if (topicFilter === 'used') topics = topics.filter(t => t.used);
if (topicCat !== 'all') topics = topics.filter(t => t.cluster === topicCat);
document.getElementById('topics-count').textContent = topics.length + ' topics';
const el = document.getElementById('topics-list');
if (!topics.length) {
el.innerHTML = '๐ซ No topics.
';
return;
}
el.innerHTML = topics.map(t => {
const cat = CATS[t.cluster];
const canGen = !t.used && t.council_score >= 70;
return `
${canGen ? `
โถ Generate Now ` : ''}
`;
}).join('');
}
// ============================================
// ACTIONS
// ============================================
async function doCreateJob() {
const btn = document.getElementById('bc');
btn.disabled = true;
btn.textContent = 'Creating...';
try {
const d = await apiPost('/run', {});
switchTab('running');
loadJobs();
loadQueue();
showDebug('debug-home', `Job created: ${d.topic} `);
} catch (e) {
showDebug('debug-home', `${e.message} `);
} finally {
btn.disabled = false;
btn.innerHTML = 'โถ Create Video';
}
}
async function doGenerateTopic() {
const btn = document.getElementById('bg');
btn.disabled = true;
btn.textContent = 'Generating...';
try {
const topic = prompt('Topic idea:', '');
if (topic === null) {
btn.disabled = false;
btn.innerHTML = 'โฆ Generate Topic';
return;
}
// This would call your topic council - for now just show message
showDebug('debug-home', 'Topic generation triggered (implement topic council call) ');
loadQueue();
} catch (e) {
showDebug('debug-home', `${e.message} `);
} finally {
btn.disabled = false;
btn.innerHTML = 'โฆ Generate Topic';
}
}
async function doKillIncomplete() {
const run = allJobs.filter(j => ['pending', 'processing', 'images', 'voice', 'render', 'upload'].includes(j.status));
if (!run.length) {
showDebug('debug-home', 'No incomplete jobs. ');
return;
}
if (!confirm(`Kill ${run.length} job(s)?`)) return;
try {
const d = await apiPost('/kill-incomplete', {});
showDebug('debug-home', `Killed ${d.killed}. Restored: ${d.topics_restored} `);
setTimeout(() => { loadJobs(); loadQueue(); }, 600);
} catch (e) {
showDebug('debug-home', `${e.message} `);
}
}
async function doRestoreFailed() {
const f = allJobs.filter(j => j.status === 'failed');
if (!f.length) {
showDebug('debug-home', 'No failed jobs. ');
return;
}
if (!confirm(`Restore ${f.length} jobs?`)) return;
try {
const d = await apiPost('/restore-failed', {});
showDebug('debug-home', `Restored ${d.restored}. `);
setTimeout(() => { loadJobs(); loadQueue(); }, 600);
} catch (e) {
showDebug('debug-home', `${e.message} `);
}
}
async function doTestAPI() {
const btn = document.getElementById('bt');
if (btn) {
btn.disabled = true;
btn.textContent = 'Testing...';
}
try {
const health = await apiGet('/health');
const config = await apiGet('/config');
showDebug('debug-home', `API: ${health.version} R2: ${config.r2_base_url ? 'OK' : 'Not set'} `);
} catch (e) {
showDebug('debug-home', `API Error: ${e.message} `);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = 'โก Test API';
}
}
}
async function doSyncAnalytics() {
try {
await apiPost('/sync-analytics', {});
showDebug('debug-home', 'Sync started. ');
setTimeout(loadAnalytics, 8000);
} catch (e) {
alert(e.message);
}
}
async function setVPD(n) {
try {
await apiPost('/system-state', { videos_per_day: n });
loadSystemState();
showDebug('debug-home', `Schedule: ${n} video${n > 1 ? 's' : ''}/day `);
} catch (e) {
showDebug('debug-home', `${e.message} `);
}
}
async function toggleVoiceMode() {
const newMode = currentVoiceMode === 'ai' ? 'human' : 'ai';
try {
const d = await apiPost('/system-state', { voice_mode: newMode });
currentVoiceMode = d.voice_mode || newMode;
setVoiceModeUI(currentVoiceMode);
showDebug('debug-home', currentVoiceMode === 'human' ? '๐ค Human Voice Mode ON ' : '๐ค AI Voice Mode ');
} catch (e) {
showDebug('debug-home', `${e.message} `);
}
}
function setVoiceModeUI(mode) {
const h = mode === 'human';
document.getElementById('vm-tog').style.background = h ? 'var(--green)' : 'var(--accent)';
document.getElementById('vm-knob').style.transform = h ? 'translateX(16px)' : 'translateX(0)';
document.getElementById('vm-lbl').textContent = h ? '๐ค HUMAN VOICE' : '๐ค AI VOICE';
document.getElementById('vm-lbl').style.color = h ? 'var(--green)' : 'var(--accent)';
const banner = document.getElementById('staging-banner');
if (banner) {
banner.style.display = h ? 'none' : 'block';
banner.innerHTML = '๐ค AI Voice Mode โ pipeline auto-completes. Switch to Human Voice to use staging. ';
}
}
async function togglePublish() {
const knb = document.getElementById('pub-knob');
const isOn = knb.style.transform === 'translateX(16px)';
const newState = !isOn;
setPublishUI(newState);
try {
await apiPost('/system-state', { publish: newState });
} catch (e) {
setPublishUI(isOn);
alert('Failed: ' + e.message);
}
}
function setPublishUI(on) {
document.getElementById('pub-tog').style.background = on ? 'var(--green)' : 'var(--red)';
document.getElementById('pub-knob').style.transform = on ? 'translateX(16px)' : 'translateX(0)';
document.getElementById('pub-lbl').textContent = on ? 'PUBLISH ON' : 'PUBLISH OFF';
document.getElementById('pub-lbl').style.color = on ? 'var(--green)' : 'var(--red)';
}
async function retryUpload(jobId, btn) {
btn.disabled = true;
btn.textContent = 'Retrying...';
try {
await apiPost('/publish-job', { job_id: jobId });
btn.textContent = 'โ Retrying!';
btn.style.background = 'var(--green)';
setTimeout(loadCBDP, 2000);
} catch (e) {
btn.textContent = 'Retry Failed';
btn.disabled = false;
alert(e.message);
}
}
async function publishCBDP(jobId, btn) {
if (!confirm('Publish this video to YouTube now?')) return;
btn.disabled = true;
btn.textContent = 'โณ Publishing...';
try {
await apiPost('/publish-job', { job_id: jobId });
btn.textContent = 'โ Sent!';
btn.style.background = 'var(--green)';
setTimeout(() => {
loadCBDPReview();
loadJobs();
}, 1500);
} catch (e) {
btn.textContent = '๐ Publish';
btn.disabled = false;
alert('Publish failed: ' + e.message);
}
}
async function rejectCBDP(jobId, btn) {
if (!confirm('Reject this video? The topic will return to queue for reuse.')) return;
btn.disabled = true;
btn.textContent = 'โณ...';
try {
await apiPost('/reject-job', { job_id: jobId });
loadCBDPReview();
loadQueue();
} catch (e) {
btn.textContent = 'โ Reject';
btn.disabled = false;
alert('Reject failed: ' + e.message);
}
}
async function generateNow(topicId, btn) {
if (!confirm('Generate a video from this topic right now?')) return;
btn.disabled = true;
btn.textContent = 'โณ Creating...';
try {
const d = await apiPost('/run-topic', { topic_id: topicId });
btn.textContent = 'โ Job created!';
btn.style.color = 'var(--green)';
showDebug('debug-home', `Video job created from topic: ${d.topic} `);
setTimeout(() => { loadJobs(); loadQueue(); renderTopicsPage(); }, 800);
} catch (e) {
btn.textContent = 'โถ Generate Now';
btn.disabled = false;
alert('Failed: ' + e.message);
}
}
// ============================================
// IMAGE LIBRARY
// ============================================
async function uploadLibImages(input) {
const files = Array.from(input.files);
if (!files.length) return;
const btn = input.parentElement;
const orig = btn.innerHTML;
btn.style.color = 'var(--yellow)';
const topic = prompt('Tag these images with a topic name (used for filtering):', 'uploaded') || 'uploaded';
let ok = 0, fail = 0;
for (let i = 0; i < files.length; i++) {
const f = files[i];
btn.innerHTML = `โณ ${f.name.slice(0, 20)}... (${i + 1}/${files.length})`;
try {
const r = await fetch(`${API_BASE}/upload-image?topic=${encodeURIComponent(topic)}&filename=${encodeURIComponent(f.name)}`, {
method: 'POST',
headers: { 'Content-Type': f.type || 'image/png' },
body: f
});
const d = await r.json();
if (d.error) throw new Error(d.error);
ok++;
} catch (e) {
console.error('Upload failed:', f.name, e);
fail++;
}
}
btn.innerHTML = orig;
btn.style.color = '';
input.value = '';
const msg = `โ Uploaded ${ok} image${ok !== 1 ? 's' : ''}${fail ? ` (โ ${fail} failed)` : ''}`;
showDebug('debug-home', `${msg} `);
loadLibrary();
}
function toggleLibImage(el) {
const url = el.dataset.imgurl;
const idx = selectedImages.indexOf(url);
if (idx > -1) {
selectedImages.splice(idx, 1);
} else {
if (selectedImages.length >= 3) {
alert('Select exactly 3 images. Deselect one first.');
return;
}
selectedImages.push(url);
}
document.getElementById('lib-sel-count').textContent = selectedImages.length + ' / 3 selected';
const btn = document.getElementById('lib-create-btn');
btn.disabled = selectedImages.length !== 3;
btn.style.opacity = selectedImages.length === 3 ? '1' : '.4';
renderLibrary();
}
async function createVideoFromLibrary() {
if (selectedImages.length !== 3) {
alert('Select exactly 3 images first.');
return;
}
const btn = document.getElementById('lib-create-btn');
btn.disabled = true;
btn.textContent = 'โณ Creating...';
try {
const d = await apiPost('/run-with-images', { image_urls: selectedImages });
selectedImages = [];
renderLibrary();
document.getElementById('lib-sel-count').textContent = '0 / 3 selected';
btn.textContent = 'โ Job created!';
btn.style.color = 'var(--green)';
showDebug('debug-home', 'Video job created from library images ');
setTimeout(() => { loadJobs(); showPage('home', document.querySelector('.nav-btn')); }, 1200);
} catch (e) {
btn.textContent = 'โถ Create Video';
btn.disabled = false;
btn.style.opacity = '1';
alert('Failed: ' + e.message);
}
}
// ============================================
// STUDIO
// ============================================
async function openStudio(jobId) {
studioJob = allStaged.find(j => j.id === jobId);
if (!studioJob) return;
document.getElementById('stu-title').textContent = studioJob.topic || 'Studio';
document.getElementById('stu-id').textContent = jobId;
document.getElementById('stu-script').textContent = (studioJob.script_package && studioJob.script_package.text) || 'No script';
const vid = document.getElementById('stu-vid');
const videoUrl = studioJob.video_r2_url && R2_BASE_URL ? `${R2_BASE_URL}/${studioJob.video_r2_url}` : '';
if (videoUrl) {
vid.src = videoUrl;
vid.load();
vid.onerror = () => {
vid.style.display = 'none';
document.getElementById('stu-vid-err').style.display = 'flex';
};
vid.oncanplay = () => {
vid.style.display = '';
document.getElementById('stu-vid-err').style.display = 'none';
};
} else {
vid.removeAttribute('src');
document.getElementById('stu-vid-err').style.display = 'flex';
}
await loadMusicList();
resetRec();
document.getElementById('studio').classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function closeStudio() {
document.getElementById('studio').classList.add('hidden');
document.body.style.overflow = '';
stopRec();
if (playbackAudio) {
playbackAudio.pause();
playbackAudio = null;
}
studioJob = null;
}
async function loadMusicList() {
try {
const d = await apiGet('/music-library');
const icons = { Epic: 'โก', Hopeful: '๐
', Tech: '๐ป', Emotional: '๐ซ', Neutral: '๐ต' };
document.getElementById('music-list').innerHTML = d.tracks.map(t => `
${icons[t.category] || '๐ต'}
${t.label}
${t.category} ยท ${t.duration}s
${selectedMusic === t.id ? 'โ' : ''}
`).join('');
} catch (e) {
document.getElementById('music-list').innerHTML = 'Music unavailable
';
}
}
function selectMusic(id) {
selectedMusic = id;
loadMusicList();
}
function setChar(el, preset) {
selectedPreset = preset;
document.querySelectorAll('.char-btn').forEach(b => b.classList.remove('active'));
el.classList.add('active');
document.getElementById('char-desc').textContent = CHAR[preset] || '';
}
// ============================================
// RECORDER
// ============================================
async function startRec() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, sampleRate: 44100 }
});
audioCtx = new AudioContext({ sampleRate: 44100 });
const src = audioCtx.createMediaStreamSource(stream);
analyserNode = audioCtx.createAnalyser();
analyserNode.fftSize = 2048;
const hpf = audioCtx.createBiquadFilter();
hpf.type = 'highpass';
hpf.frequency.value = 80;
const comp = audioCtx.createDynamicsCompressor();
comp.threshold.value = -24;
comp.ratio.value = 4;
comp.attack.value = 0.003;
comp.release.value = 0.25;
const lim = audioCtx.createDynamicsCompressor();
lim.threshold.value = -3;
lim.ratio.value = 20;
lim.attack.value = 0.001;
lim.release.value = 0.1;
src.connect(hpf);
hpf.connect(comp);
comp.connect(analyserNode);
analyserNode.connect(lim);
lim.connect(audioCtx.destination);
drawWaveform();
audioChunks = [];
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
mediaRecorder.ondataavailable = e => {
if (e.data.size > 0) audioChunks.push(e.data);
};
mediaRecorder.onstop = () => {
recordedBlob = new Blob(audioChunks, { type: 'audio/webm' });
document.getElementById('rec-ply').disabled = false;
document.getElementById('rec-rst').disabled = false;
document.getElementById('rec-status').textContent = `โ Recorded (${Math.round(recordedBlob.size / 1024)}KB)`;
document.getElementById('rec-status').className = 'rec-status';
clearInterval(recTimer);
};
mediaRecorder.start(100);
isRecording = true;
recSecs = 0;
recTimer = setInterval(() => {
recSecs++;
const m = Math.floor(recSecs / 60);
const s = recSecs % 60;
document.getElementById('rec-dur').textContent = m + ':' + (s < 10 ? '0' : '') + s;
}, 1000);
document.getElementById('rec-rec').disabled = true;
document.getElementById('rec-stp').disabled = false;
document.getElementById('rec-status').textContent = 'โ RECORDING...';
document.getElementById('rec-status').className = 'rec-status recording';
} catch (e) {
alert('Microphone error: ' + e.message);
}
}
function stopRec() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(t => t.stop());
}
isRecording = false;
document.getElementById('rec-rec').disabled = false;
document.getElementById('rec-stp').disabled = true;
}
function playRec() {
if (!recordedBlob) return;
if (playbackAudio) {
playbackAudio.pause();
playbackAudio = null;
document.getElementById('rec-ply').textContent = 'โถ';
return;
}
playbackAudio = new Audio(URL.createObjectURL(recordedBlob));
playbackAudio.play();
document.getElementById('rec-ply').textContent = 'โธ';
playbackAudio.onended = () => {
document.getElementById('rec-ply').textContent = 'โถ';
playbackAudio = null;
};
}
function resetRec() {
stopRec();
if (playbackAudio) {
playbackAudio.pause();
playbackAudio = null;
}
audioChunks = [];
recordedBlob = null;
recSecs = 0;
document.getElementById('rec-rec').disabled = false;
document.getElementById('rec-stp').disabled = true;
document.getElementById('rec-ply').disabled = true;
document.getElementById('rec-rst').disabled = true;
document.getElementById('rec-status').textContent = 'Ready';
document.getElementById('rec-status').className = 'rec-status';
document.getElementById('rec-dur').textContent = '0:00';
const c = document.getElementById('waveform');
if (c) {
const ctx2 = c.getContext('2d');
ctx2.clearRect(0, 0, c.width, c.height);
}
}
function drawWaveform() {
if (!analyserNode) return;
const canvas = document.getElementById('waveform');
const ctx2 = canvas.getContext('2d');
const W = canvas.width = canvas.offsetWidth;
const H = canvas.height;
const buf = new Uint8Array(analyserNode.frequencyBinCount);
function draw() {
if (!isRecording) return;
requestAnimationFrame(draw);
analyserNode.getByteTimeDomainData(buf);
ctx2.fillStyle = 'rgba(13,19,32,0.4)';
ctx2.fillRect(0, 0, W, H);
ctx2.lineWidth = 1.5;
ctx2.strokeStyle = '#00e5ff';
ctx2.beginPath();
const step = W / buf.length;
for (let i = 0; i < buf.length; i++) {
const y = (buf[i] / 128.0) * (H / 2);
i === 0 ? ctx2.moveTo(0, y) : ctx2.lineTo(i * step, y);
}
ctx2.stroke();
}
draw();
}
function previewMix() {
const vid = document.getElementById('stu-vid');
if (vid && vid.src) {
vid.currentTime = 0;
vid.play().catch(e => console.warn('Preview play failed:', e));
}
if (recordedBlob) {
if (playbackAudio) {
playbackAudio.pause();
playbackAudio = null;
}
playbackAudio = new Audio(URL.createObjectURL(recordedBlob));
playbackAudio.play();
}
}
// ============================================
// PUBLISH
// ============================================
async function doPublish(publishAt) {
if (!studioJob) {
alert('No job open');
return;
}
if (!recordedBlob) {
alert('Please record your voice first');
return;
}
const sEl = document.getElementById('pub-status');
const n = document.getElementById('pub-now');
const s = document.getElementById('pub-sch');
n.disabled = s.disabled = true;
sEl.textContent = 'โณ Uploading voice...';
sEl.style.color = 'var(--yellow)';
try {
// Upload voice
const ur = await fetch(`${API_BASE}/upload-voice?job_id=${studioJob.id}`, {
method: 'POST',
body: recordedBlob,
headers: { 'Content-Type': 'audio/webm' }
});
if (!ur.ok) throw new Error('Upload failed: ' + ur.status);
sEl.textContent = 'โณ Starting mix...';
// Start mix
const mr = await apiPost('/mix', {
job_id: studioJob.id,
music_track: selectedMusic || 'neutral_01',
music_volume: (parseInt(document.getElementById('mus-vol').value) || 8) / 100,
publish_at: publishAt || null,
voice_offset_ms: parseInt(document.getElementById('voice-off').value) || 0
});
sEl.textContent = 'โ ' + (publishAt ? 'Scheduled!' : 'Publishing soon!');
sEl.style.color = 'var(--green)';
allStaged = allStaged.filter(j => j.id !== studioJob.id);
renderStagingGrid();
setTimeout(closeStudio, 2000);
} catch (e) {
sEl.textContent = 'โ ' + e.message;
sEl.style.color = 'var(--red)';
} finally {
n.disabled = s.disabled = false;
}
}
function publishNow() {
doPublish(null);
}
function publishScheduled() {
const dt = document.getElementById('pub-at').value;
if (!dt) {
alert('Pick a date/time first');
return;
}
doPublish(new Date(dt).toISOString());
}
// ============================================
// CALENDAR NAV
// ============================================
function calPrev() {
calDate = new Date(calDate.getFullYear(), calDate.getMonth() - 1, 1);
renderCalendar();
}
function calNext() {
calDate = new Date(calDate.getFullYear(), calDate.getMonth() + 1, 1);
renderCalendar();
}
function calToday() {
calDate = new Date();
renderCalendar();
}
// ============================================
// MODALS
// ============================================
function openReplenishModal() {
document.getElementById('rep-modal').classList.remove('hidden');
}
function closeReplenishModal() {
document.getElementById('rep-modal').classList.add('hidden');
}
async function doReplenish() {
const cats = Array.from(document.querySelectorAll('#modal-cats .cat-check.selected')).map(d => d.dataset.cat);
const target = parseInt(document.getElementById('tgt-slider').value);
closeReplenishModal();
showDebug('debug-home', `Replenishing [${cats.join(', ')}] target ${target}... `);
try {
// This would call your topic council
showDebug('debug-home', 'Replenish triggered (implement topic council call) ');
setTimeout(loadQueue, 5000);
} catch (e) {
showDebug('debug-home', `${e.message} `);
}
}
function filterTopics(f) {
topicFilter = f;
['all', 'ready', 'used'].forEach(k => {
const b = document.getElementById('bt-' + k);
if (b) b.className = 'btn ' + (k === f ? 'btn-primary' : 'btn-ghost');
});
renderTopicsPage();
}
// ============================================
// INIT
// ============================================
function init() {
buildCatStrips();
loadConfig();
loadAll();
setInterval(() => {
loadJobs();
loadQueue();
loadStaging();
loadCBDP();
loadCBDPReview();
if (currentPage === 'analytics') loadAnalytics();
if (currentPage === 'calendar') renderCalendar();
}, 6000);
}
function loadAll() {
loadJobs();
loadQueue();
loadSystemState();
loadAnalytics();
loadCalendar();
loadStaging();
loadCBDP();
loadCBDPReview();
}
// Start
init();